#include "stdafx.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include "Communicator.h"
#include "core/Game.h"
#include "state/RunningState.h"
#include "player/Player.h"
#include "player/PlayerContainer.h"
using boost::asio::ip::tcp;

/* TODO 
	-  
	- Create logic for the initial client-server communication. Server sends player ID to client etc. This communication can be done immediately and block [x]
	- Create the server-clients communication in the PendingState. Initially just have the server send a message for all clients to starts when it enter the running state [x]
	- Create the server-clients communication in the runningsState. Make Use of previously written code. 
		- When first entering running state we need to create player objects 
*/

namespace tcpCom {
	void send(std::vector<unsigned char>& data, tcp::socket& socket, bool sendSize = false)
	{
		boost::system::error_code writeError;
		if (sendSize) {
			int size = data.size();
			boost::asio::write(socket, boost::asio::buffer(&size, sizeof(int)), writeError);
			if (writeError)
				throw std::runtime_error(writeError.message() + "\n");
		}
				
		boost::asio::write(socket, boost::asio::buffer(data), writeError);
		if (writeError)
			throw std::runtime_error(writeError.message() + "\n");
	}

	void send(void* data, unsigned int size, tcp::socket& socket, bool sendSize = false)
	{
		boost::system::error_code writeError;
		if (sendSize) {
			int sizeInt = static_cast<int>(size);
			boost::asio::write(socket, boost::asio::buffer(&sizeInt, sizeof(int)), writeError);
			if (writeError)
				throw std::runtime_error(writeError.message() + "\n");
		}

		boost::asio::write(socket, boost::asio::buffer(data, size), writeError);
		if (writeError)
			throw std::runtime_error(writeError.message()+ "\n");
	}

	void receiveKnown(void* buffer, unsigned int size, tcp::socket& socket)
	{
		boost::system::error_code readError;
		
		boost::asio::read(socket, boost::asio::buffer(buffer, size), boost::asio::transfer_all(), readError);
		if (readError)
			throw std::runtime_error(readError.message()+ "\n");
	}

	void receiveUnknown(std::vector<unsigned char>& buffer, tcp::socket& socket)
	{
		boost::system::error_code readError;
		/* Get the size of incoming data */
		int incomingSize = -1;
		boost::asio::read(socket, boost::asio::buffer(&incomingSize, sizeof(int)), boost::asio::transfer_all(), readError);
		if (readError)
			throw std::runtime_error(readError.message()+ "\n");

		/* read data */ 
		resizeIf(buffer, incomingSize);
		boost::asio::read(socket, boost::asio::buffer(&buffer[0], incomingSize), boost::asio::transfer_all(), readError);
		if (readError)
			throw std::runtime_error(readError.message()+ "\n");
	}
}

class ComImpl {
public:
	virtual void update(float dt, State::Type type) = 0;
	virtual ~ComImpl() {};
};

class ComServer : public ComImpl  {
public:
	ComServer() : clientAcceptor(Communicator::getIoService(), tcp::endpoint(tcp::v4(), 13)) { 
		 connAcceptThread = boost::thread(boost::bind(&ComServer::startAccept, this));
	} 
	virtual ~ComServer() {
		boost::system::error_code err;
		clientAcceptor.close(err); // force the acceptor to let go 
		connAcceptThread.join(); // wait for the accept thread to finish 
	}
private:
	void startAccept() {
		for (;;) {
			try {
				boost::shared_ptr<tcp::socket> socket(new tcp::socket(clientAcceptor.get_io_service()));
				clientAcceptor.accept(*socket); // thread blocks here 
				clientSockets.push_back(socket); // sync?
				/*clientBufs.push_back(std::vector<unsigned char>());*/
				std::printf("This server has added a new client,\nTODO: support more than one client!\n");

				handShake(socket);
			} catch (std::exception& e)	{
				std::cerr << e.what() << std::endl;
				return;
			}
		}
	}
	void update(float dt, State::Type type) {
		Game* game = Game::getInstance();

		if ( type == State::PENDING  ) {
			/*todo: add lock*/
			
			if ( InputManager::isKeyClickedLocal(GLFW_KEY_SPACE) /*clientSockets.size() <= 0*/) {
				Packets::GamePendingPacket pendingPacket(Packets::GamePendingPacket::READY);
				for ( unsigned int i = 0; i < clientSockets.size(); ++i ) 
					tcpCom::send(&pendingPacket, sizeof(pendingPacket), *clientSockets[i]);	
				sendGameStartInfo();
			}
			else {
				Packets::GamePendingPacket pendingPacket(Packets::GamePendingPacket::NONE);
				for ( unsigned int i = 0; i < clientSockets.size(); ++i ) 
					tcpCom::send(&pendingPacket, sizeof(pendingPacket), *clientSockets[i]);	
			}
		}
		else if (type == State::RUNNING) {
			SyncGameState();
		}
	}
private:
	void handShake(boost::shared_ptr<tcp::socket> & socket) {
		static int playerID = 0;
		playerID++;
		// initial test version, just works with one client
		Packets::WelcomePacket packet(playerID);
		tcpCom::send(&packet, sizeof(packet), *socket);
		clientIds.push_back(playerID);
		clientBufs.insert(std::pair<int, std::vector<unsigned char>>(playerID, std::vector<unsigned char>() ));
	}

	void SyncGameState() {

		LOG("Syncing with clients..\n");

		RunningState* rs = Game::getInstance()->getRunningState();
		// read all sockets for player input data 
		for ( unsigned int i = 0; i < clientSockets.size(); ++i ) 
		{
			Packets::PlayerStatePacket playerPacket;
			tcpCom::receiveKnown(&playerPacket, sizeof(Packets::PlayerStatePacket), *clientSockets[i]);
			Player* player = rs->playerContainer->getPlayer(playerPacket.playerId);
			tcpCom::receiveUnknown(clientBufs[playerPacket.playerId], *clientSockets[i]); // take the serialized player data and place it in our externalPlayer object 
			player->decode(clientBufs[playerPacket.playerId]);
		}
		
		for ( unsigned int i = 0; i < clientSockets.size(); ++i ) {
			// write all data from all players to this client
			for ( unsigned int j = 0; j < rs->playerContainer->externalPlayers.size(); ++j ) {
				Packets::PlayerStatePacket playerPacket(rs->playerContainer->externalPlayers[j]->getId());
				tcpCom::send(&playerPacket, sizeof(playerPacket), *clientSockets[i]);            
				/* Input data of this client */
				tcpCom::send(clientBufs[playerPacket.playerId], *clientSockets[i], true);
			}

			/* write server local player data to this client */
			Packets::PlayerStatePacket meStatePacket(rs->playerContainer->localPlayer->getId());
			tcpCom::send(&meStatePacket, sizeof(meStatePacket), *clientSockets[i]);            
			/* serialize and send my input data */
			rs->playerContainer->localPlayer->encode(localPlayerBuf);
			tcpCom::send(localPlayerBuf, *clientSockets[i], true);
		}
		LOG("Done syncing..\n");
	}

	void sendGameStartInfo() {
		int numbPlayers = clientSockets.size()+1;
		Packets::GameInfoPacket packet(numbPlayers); // me and clients 

		for ( unsigned int i = 0; i < clientSockets.size(); ++i) {
			tcpCom::send(&packet, sizeof(packet), *clientSockets[i]);
		}
		
		std::vector<Packets::PlayerStartInfo> startInfoList(numbPlayers);
		startInfoList[0] = Packets::PlayerStartInfo(0, Packets::PlayerStartInfo::start_north, 0, 0xFFFF0000);
		
		
		int colors[] = { 0xFF66FFFF, 0x33FFFF, 0x6600FF, 0xFF0000FF, 0xFF8C00FF, 0x98FB98FF, 0x00FFFFFF, 0xFFFFFFFF };
		for ( int i = 1; i < numbPlayers; ++i) {
			startInfoList[i] = Packets::PlayerStartInfo(clientIds[i-1], Packets::PlayerStartInfo::start_north, i, colors[(i-1) % ARRAYSIZE(colors)]);
		}
		
		for		( unsigned int i = 0; i < clientSockets.size(); ++i) {
			for ( int j = 0; j < numbPlayers; ++j) {
				tcpCom::send(&startInfoList[j], sizeof(Packets::PlayerStartInfo), *clientSockets[i]);
			}
		}
		
		// start by changing to running state and creating players 
		Game::getInstance()->getRunningState()->initPlayers(startInfoList, 0/*server player id always 0*/);
		Game::getInstance()->getCurrentState()->queuedState = State::RUNNING;
	}

	tcp::acceptor clientAcceptor;
	boost::thread connAcceptThread;
	std::vector<boost::shared_ptr<tcp::socket>> clientSockets;
	std::map<int, std::vector<unsigned char>> clientBufs;
	std::vector<int> clientIds;
	std::vector<unsigned char> localPlayerBuf;
};

class ComClient : public ComImpl {
public:
	ComClient() : clientSocket(Communicator::getIoService()), connected(false), id(-1) {}

	void update(float dt, State::Type type) {
		if (type == State::PENDING) {
			if (!connected) {
				attemptConnect();
			} else {
				SyncPendingState();
			}
		}
		else if (type == State::RUNNING) {
			SyncGameState();
		}
	}
private:

	void SyncGameState() {
		LOG("Syncing with server..\n");

		RunningState* rs = Game::getInstance()->getRunningState();
		Packets::PlayerStatePacket meStatePacket(rs->playerContainer->localPlayer->getId());
		tcpCom::send(&meStatePacket, sizeof(Packets::PlayerStatePacket), clientSocket);            
		/* Heres my input data */
		rs->playerContainer->localPlayer->encode(transferBuf);
		tcpCom::send(transferBuf, clientSocket, true);

		/* recv input data for all external players and local player */ 
		for ( unsigned int i = 0; i < rs->playerContainer->externalPlayers.size()+1; ++i )
		{
			LOG("starting..");
			Packets::PlayerStatePacket playerPacket;
			tcpCom::receiveKnown(&playerPacket, sizeof(Packets::PlayerStatePacket), clientSocket);
			Player* player = rs->playerContainer->getPlayer(playerPacket.playerId);
			tcpCom::receiveUnknown(transferBuf, clientSocket); // take the serialized player data and place it in our externalPlayer object 
			player->decode(transferBuf);
			LOG(" recvd info on player\n");
		}

		LOG("Syncing with server..\n");
	}

	void SyncPendingState() {

		Packets::GamePendingPacket pendingPacket;
		tcpCom::receiveKnown(&pendingPacket, sizeof(pendingPacket), clientSocket);

		if (pendingPacket.msg == Packets::GamePendingPacket::READY)
			getGameStartInfo();
	}

	bool attemptConnect() {
		try {
			tcp::resolver resolver(Communicator::getIoService());
			tcp::resolver::query query("localhost", "13");

			tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);

			/* establish connection to socket */ 
			boost::asio::connect(clientSocket, endpoint_iterator);
			std::printf("This client has connected to a server!\n");

			handShake();

			return true;
		} catch (std::exception& e)	{
			std::cerr << e.what() << std::endl;
			return false;
		}
	}

	void handShake() {
		Packets::WelcomePacket packet;
		tcpCom::receiveKnown(&packet, sizeof(packet), clientSocket);
		id = packet.id;
		std::printf("handshake complete, my id is: %i\n", id);
		connected = true;
	}

	void getGameStartInfo() {
		Packets::GameInfoPacket packet;
		tcpCom::receiveKnown(&packet, sizeof(packet), clientSocket);
		
		std::vector<Packets::PlayerStartInfo> startInfoList(packet.numbPlayers);

		for ( int i = 0; i < packet.numbPlayers; ++i) {
			tcpCom::receiveKnown(&startInfoList[i], sizeof(Packets::PlayerStartInfo), clientSocket);
		}

		Game::getInstance()->getRunningState()->initPlayers(startInfoList, id);
		Game::getInstance()->getCurrentState()->queuedState = State::RUNNING;
	}

	std::vector<unsigned char> transferBuf;
	tcp::socket clientSocket;
	bool connected;
	int id;
};

Communicator::Communicator(bool isServer)
{
	if (isServer)
		impl = new ComServer();
	else 
		impl = new ComClient();
}

Communicator::~Communicator() {
	delete impl;
};

void Communicator::update(float dt, State::Type type) {
	impl->update(dt, type);
}

boost::asio::io_service& Communicator::getIoService() { 
	static bool started = false;
	static boost::asio::io_service ioService;

	if (!started) {
		ioService.run();
		started = true;
	}
	return ioService;
}

#if 0 
	try {
		if ( isServer ) {
			server = new tcp_server();
		} else {
			client = new GameClient();
			client->connect();
		}
	}
	catch (std::exception& e) {
		std::cerr << e.what() << std::endl;
	}
#endif 

#if 0 
#if 0 
void Communicator::imReady()
{
	using Packets::GamePendingPacket;
	GamePendingPacket packet(GamePendingPacket::READY);

	if ( isServer ) 
	{
		server->send(&packet, sizeof(packet));
	}
	else
	{
		client->send(&packet, sizeof(packet));
	}
}
#endif 

PendingComImpl::PendingComImpl(bool isServer) : Communicator(isServer) {

}

void PendingComImpl::update(float dt) {
	using Packets::GamePendingPacket;

	if (isServer) {

	}
	else {

	}
}

RunningComImpl::RunningComImpl(bool isServer) : Communicator(isServer) {

}

void RunningComImpl::update(float dt) {
	using Packets::GamePendingPacket;

	if (isServer) {

	}
	else {

	}
}

#if 0
void Communicator::update(float dt) 
{
    using Packets::PlayerStatePacket;
    using Packets::GamePendingPacket;

    if ( mpStarted )
    {
        if ( isServer ) 
        {
            server->reset();
            for ( ;; ) // read all sockets for player input data 
            {
                PlayerStatePacket playerPacket;
                server->receiveKnown(&playerPacket, sizeof(PlayerStatePacket));
                Player* player = gm->playerContainer->getExternalPlayer(playerPacket.playerId);
                server->receiveUnknown(player->serializedBuffer); // take the serialized player data and place it in our externalPlayer object 
                server->next();
            }
            server->reset();
            
            for ( ;; ) 
            {
                // write all player data to this client
                for ( int i = 0; gm->playerContainer->externalPlayers.size(); ++i ) 
                {
                    PlayerStatePacket playerPacket(gm->playerContainer->externalPlayers[i]->getId(), 0);
                    server->send(&playerPacket, sizeof(playerPacket));            
                    /* Input data of this client */
                    server->send(gm->playerContainer->localPlayer->getSerialized());
                }
                /* write all my local player data to this client */
                PlayerStatePacket meStatePacket(gm->playerContainer->localPlayer->getId(), 0);
                server->send(&meStatePacket, sizeof(meStatePacket));            
                /* Heres my input data */
                server->send(gm->playerContainer->localPlayer->getSerialized());
                /* goto next client */
                server->next(); 
            }
        }
        else
        {
            PlayerStatePacket meStatePacket(gm->playerContainer->localPlayer->getId(), 0);
            client->send(&meStatePacket, sizeof(meStatePacket));            
            /* Heres my input data */
            client->send(gm->playerContainer->localPlayer->getSerialized());

            /* recv input data for all external players */ 
            for ( unsigned int i = 0; i < gm->playerContainer->externalPlayers.size(); ++i )
            {
                PlayerStatePacket playerPacket;
                client->receiveKnown(&playerPacket, sizeof(PlayerStatePacket));
                Player* player = gm->playerContainer->getExternalPlayer(playerPacket.playerId);
                client->receiveUnknown(player->serializedBuffer); // take the serialized player data and place it in our externalPlayer object 
            }
        }
    }
    else // game pending
    {
        if ( isServer ) 
        {
            // listen to clients for gamepending packets, telling if theyre ready or not 
            // if all clients+me is ready -> initiate game by sending pendingpacket with GAMESTART enum value 
            //server->communicate();
        }
        else
        {
            if ( isReady ) 
            {
                GamePendingPacket packet(GamePendingPacket::READY);
                client->send(&packet, sizeof(packet));
            }
        }
    }
}
#endif
#endif 